أتقن ملفات تعريف TypeScript (.d.ts) لإطلاق العنان للأمان النوعي والإكمال التلقائي لأي مكتبة JavaScript. تعلم كيفية استخدام @types، وإنشاء تعريفاتك الخاصة، والتعامل مع التعليمات البرمجية للمكتبات الخارجية باحترافية.
إطلاق العنان لنظام JavaScript البيئي: نظرة عميقة على ملفات تعريف TypeScript
أحدثت TypeScript ثورة في تطوير الويب الحديث من خلال جلب الكتابة الثابتة (static typing) إلى عالم JavaScript الديناميكي. يوفر هذا الأمان النوعي فوائد مذهلة: اكتشاف الأخطاء في وقت الترجمة (compile-time)، وتمكين الإكمال التلقائي القوي في المحرر، وجعل قواعد التعليمات البرمجية الكبيرة أكثر قابلية للصيانة بشكل كبير. ومع ذلك، ينشأ تحدٍ كبير عندما نريد استخدام النظام البيئي الواسع لمكتبات JavaScript الحالية - والتي لم يُكتب معظمها بـ TypeScript. كيف يفهم كود TypeScript المكتوب بدقة أنواع الأشكال والدوال والمتغيرات من مكتبة JavaScript غير المكتوبة بنظام الأنواع؟
يكمن الجواب في ملفات تعريف TypeScript (TypeScript Declaration Files). هذه الملفات، التي يمكن التعرف عليها من خلال امتدادها .d.ts، هي الجسر الأساسي بين عالمي TypeScript وJavaScript. إنها تعمل كمخطط أو عقد API، حيث تصف أنواع مكتبة خارجية دون أن تحتوي على أي من تطبيقاتها الفعلية. في هذا الدليل الشامل، سنستكشف كل ما تحتاج إلى معرفته لإدارة تعريفات الأنواع بثقة لأي مكتبة JavaScript في مشاريع TypeScript الخاصة بك.
ما هي ملفات تعريف TypeScript بالضبط؟
تخيل أنك وظفت مقاولاً يتحدث لغة مختلفة فقط. للعمل معه بفعالية، ستحتاج إلى مترجم أو مجموعة مفصلة من التعليمات بلغة تفهمانها كلاكما. يخدم ملف التعريف هذا الغرض بالضبط لمترجم TypeScript (المقاول).
يحتوي ملف .d.ts على معلومات النوع فقط. ويتضمن:
- توقيعات الدوال والأساليب (أنواع المعلمات، أنواع الإرجاع).
- تعريفات للمتغيرات وأنواعها.
- واجهات (Interfaces) وأسماء مستعارة للأنواع (type aliases) للكائنات المعقدة.
- تعريفات الفئات (Classes)، بما في ذلك خصائصها وأساليبها.
- هياكل مساحات الأسماء (Namespace) والوحدات (Module).
والأهم من ذلك، أن هذه الملفات لا تحتوي على أي كود قابل للتنفيذ. إنها مخصصة للتحليل الثابت فقط. عندما تستورد مكتبة JavaScript مثل Lodash إلى مشروع TypeScript الخاص بك، يبحث المترجم عن ملف تعريف مطابق. إذا وجد واحدًا، يمكنه التحقق من صحة الكود الخاص بك، وتوفير إكمال تلقائي ذكي، والتأكد من أنك تستخدم المكتبة بشكل صحيح. إذا لم يجده، فسيظهر خطأ مثل: Could not find a declaration file for module 'lodash'.
لماذا تعتبر ملفات التعريف غير قابلة للتفاوض في التطوير الاحترافي
إن استخدام مكتبات JavaScript بدون تعريفات أنواع مناسبة في مشروع TypeScript يقوض السبب الأساسي لاستخدام TypeScript في المقام الأول. دعنا نفكر في سيناريو بسيط باستخدام مكتبة الأدوات المساعدة الشهيرة، Lodash.
العالم بدون تعريفات الأنواع
بدون ملف تعريف، ليس لدى TypeScript أي فكرة عن ماهية lodash أو ما تحتويه. وحتى لجعل الكود يترجم، قد تميل إلى استخدام حل سريع مثل هذا:
const _: any = require('lodash');
const users = [{ 'user': 'barney' }, { 'user': 'fred' }];
// الإكمال التلقائي؟ لا مساعدة هنا.
// التحقق من النوع؟ لا. هل 'username' هو الخاصية الصحيحة؟
// يسمح المترجم بذلك، لكنه قد يفشل في وقت التشغيل.
_.find(users, { username: 'fred' });
في هذه الحالة، المتغير _ من النوع any. هذا يخبر TypeScript بشكل فعال، "لا تتحقق من أي شيء يتعلق بهذا المتغير." تفقد جميع الفوائد: لا إكمال تلقائي، لا تحقق من أنواع الوسائط، ولا يقين بشأن نوع الإرجاع. هذه بيئة خصبة لأخطاء وقت التشغيل.
العالم مع تعريفات الأنواع
الآن، دعنا نرى ما يحدث عندما نقدم ملف التعريف الضروري. بعد تثبيت الأنواع (التي سنتناولها تاليًا)، تتحول التجربة:
import _ from 'lodash';
interface User {
user: string;
active?: boolean;
}
const users: User[] = [{ 'user': 'barney' }, { 'user': 'fred' }];
// 1. يوفر المحرر إكمالًا تلقائيًا لـ 'find' ودوال lodash الأخرى.
// 2. يظهر التمرير فوق 'find' توقيعها الكامل وتوثيقها.
// 3. ترى TypeScript أن `users` هو مصفوفة من كائنات `User`.
// 4. تعرف TypeScript أن الشرط لـ `find` على `User[]` يجب أن يتضمن `user` أو `active`.
// صحيح: TypeScript موافقة.
const fred = _.find(users, { user: 'fred' });
// خطأ: TypeScript تكتشف الخطأ!
// الخاصية 'username' غير موجودة في النوع 'User'.
const betty = _.find(users, { username: 'betty' });
الفرق شاسع كالليل والنهار. نكتسب أمانًا نوعيًا كاملاً، وتجربة مطور فائقة من خلال الأدوات، وتقليلاً كبيرًا في الأخطاء المحتملة. هذا هو المعيار الاحترافي للعمل مع TypeScript.
التسلسل الهرمي للعثور على تعريفات الأنواع
إذًا، كيف تحصل على ملفات .d.ts السحرية هذه لمكتباتك المفضلة؟ هناك عملية راسخة تغطي الغالبية العظمى من السيناريوهات.
الخطوة 1: تحقق مما إذا كانت المكتبة ترفق أنواعها الخاصة
أفضل سيناريو هو عندما تكون المكتبة مكتوبة بـ TypeScript أو يقدم المشرفون عليها ملفات تعريف رسمية داخل نفس الحزمة. أصبح هذا شائعًا بشكل متزايد للمشاريع الحديثة التي تتم صيانتها جيدًا.
كيفية التحقق:
- ثبت المكتبة كالمعتاد:
npm install axios - انظر داخل مجلد المكتبة في
node_modules/axios. هل ترى أي ملفات.d.ts؟ - تحقق من ملف
package.jsonالخاص بالمكتبة بحثًا عن حقل"types"أو"typings". يشير هذا الحقل مباشرة إلى ملف التعريف الرئيسي. على سبيل المثال، يحتوي ملفpackage.jsonالخاص بـ Axios على:"types": "index.d.ts".
إذا تم استيفاء هذه الشروط، فقد انتهيت! ستجد TypeScript هذه الأنواع المرفقة وتستخدمها تلقائيًا. لا يلزم اتخاذ أي إجراء آخر.
الخطوة 2: مشروع DefinitelyTyped (@types)
بالنسبة لآلاف مكتبات JavaScript التي لا ترفق أنواعها الخاصة، أنشأ مجتمع TypeScript العالمي موردًا رائعًا: DefinitelyTyped.
DefinitelyTyped هو مستودع مركزي يديره المجتمع على GitHub يستضيف ملفات تعريف عالية الجودة لعدد هائل من حزم JavaScript. يتم نشر هذه التعريفات في سجل npm تحت نطاق @types.
كيفية استخدامه:
إذا كانت مكتبة مثل lodash لا ترفق أنواعها الخاصة، فما عليك سوى تثبيت حزمة @types المقابلة لها كتبعية تطوير (development dependency):
npm install --save-dev @types/lodash
اصطلاح التسمية بسيط ويمكن التنبؤ به: بالنسبة لحزمة اسمها package-name، ستكون أنواعها دائمًا تقريبًا في @types/package-name. يمكنك البحث عن الأنواع المتاحة على موقع npm الإلكتروني أو مباشرة على مستودع DefinitelyTyped.
لماذا --save-dev؟ ملفات التعريف مطلوبة فقط أثناء التطوير والترجمة. لا تحتوي على أي كود وقت التشغيل، لذا لا ينبغي تضمينها في حزمة الإنتاج النهائية الخاصة بك. يضمن تثبيتها كـ devDependency هذا الفصل.
الخطوة 3: عندما لا توجد أنواع - كتابة تعريفاتك الخاصة
ماذا لو كنت تستخدم مكتبة قديمة أو متخصصة أو داخلية خاصة لا ترفق أنواعًا وليست موجودة في DefinitelyTyped؟ في هذه الحالة، تحتاج إلى أن تشمر عن ساعديك وتنشئ ملف التعريف الخاص بك. على الرغم من أن هذا قد يبدو مخيفًا، يمكنك البدء ببساطة وإضافة المزيد من التفاصيل حسب الحاجة.
الحل السريع: الإعلان المختصر للوحدة المحيطة (Shorthand Ambient Module Declaration)
في بعض الأحيان، تحتاج فقط إلى جعل مشروعك يترجم بدون أخطاء بينما تكتشف استراتيجية أنواع مناسبة. يمكنك إنشاء ملف في مشروعك (على سبيل المثال، declarations.d.ts أو types/global.d.ts) وإضافة إعلان مختصر:
// في ملف .d.ts
declare module 'some-untyped-library';
هذا يخبر TypeScript، "ثق بي، هناك وحدة تسمى 'some-untyped-library' موجودة. فقط تعامل مع كل شيء يتم استيراده منها على أنه من النوع any." هذا يسكت خطأ المترجم، ولكن كما ناقشنا، فإنه يضحي بكل الأمان النوعي لتلك المكتبة. إنه حل مؤقت، وليس حلاً طويل الأمد.
إنشاء ملف تعريف مخصص أساسي
النهج الأفضل هو البدء في تعريف أنواع الأجزاء التي تستخدمها بالفعل من المكتبة. لنفترض أن لدينا مكتبة بسيطة تسمى `string-utils` تصدر دالة واحدة.
// في node_modules/string-utils/index.js
module.exports.capitalize = (str) => str.charAt(0).toUpperCase() + str.slice(1);
يمكننا إنشاء ملف string-utils.d.ts في دليل مخصص `types` في جذر مشروعنا.
// في my-project/types/string-utils.d.ts
declare module 'string-utils' {
export function capitalize(str: string): string;
// يمكنك إضافة تعريفات دوال أخرى هنا كلما استخدمتها
// export function slugify(str: string): string;
}
الآن، نحتاج إلى إخبار TypeScript بمكان العثور على تعريفات الأنواع المخصصة لدينا. نقوم بذلك في tsconfig.json:
{
"compilerOptions": {
// ... خيارات أخرى
"baseUrl": ".",
"paths": {
"*": ["types/*"]
}
}
}
مع هذا الإعداد، عندما تقوم بـ import { capitalize } from 'string-utils'، ستجد TypeScript ملف التعريف المخصص الخاص بك وستوفر الأمان النوعي الذي حددته. يمكنك بناء هذا الملف تدريجيًا كلما استخدمت المزيد من ميزات المكتبة.
الغوص أعمق: تأليف ملفات التعريف
دعنا نستكشف بعض المفاهيم الأكثر تقدمًا التي ستواجهها عند كتابة أو قراءة ملفات التعريف.
الإعلان عن أنواع مختلفة من التصدير (Exports)
يمكن لوحدات JavaScript تصدير الأشياء بطرق مختلفة. يجب أن يتطابق ملف التعريف الخاص بك مع بنية التصدير الخاصة بالمكتبة.
- التصدير المسمى (Named Exports): هذا هو الأكثر شيوعًا. رأيناه أعلاه مع `export function capitalize(...)`. يمكنك أيضًا تصدير الثوابت والواجهات والفئات.
- التصدير الافتراضي (Default Export): للمكتبات التي تستخدم `export default`.
- UMD Globals: بالنسبة للمكتبات القديمة المصممة للعمل في المتصفحات عبر علامة
<script>، فإنها غالبًا ما تربط نفسها بالكائن العام `window`. يمكنك الإعلان عن هذه المتغيرات العامة. - `export =` و `import = require()`: هذا النحو مخصص لوحدات CommonJS القديمة التي تستخدم `module.exports = ...`. على سبيل المثال، إذا كانت المكتبة تقوم بـ `module.exports = myClass;`.
declare module 'my-lib' {
export const version: string;
export interface Options { retries: number; }
export function doSomething(options: Options): Promise
declare module 'my-default-lib' {
// لتصدير دالة افتراضية
export default function myCoolFunction(): void;
// لتصدير كائن افتراضي
// const myLib = { name: 'lib', version: '1.0' };
// export default myLib;
}
// يعلن عن متغير عام '$' من نوع معين
declare var $: JQueryStatic;
// في my-class.d.ts
declare class MyClass { constructor(name: string); }
export = MyClass;
// في app.ts الخاص بك
import MyClass = require('my-class');
const instance = new MyClass('test');
على الرغم من أنه أقل شيوعًا مع وحدات ES الحديثة، إلا أنه أمر بالغ الأهمية للتوافق مع العديد من حزم Node.js القديمة ولكن التي لا تزال مستخدمة على نطاق واسع.
توسيع الوحدات (Module Augmentation): تمديد الأنواع الموجودة
واحدة من أقوى الميزات هي توسيع الوحدات (المعروفة أيضًا باسم دمج الإعلانات). يتيح لك هذا إضافة خصائص إلى الواجهات الحالية المحددة في ملف تعريف حزمة أخرى. هذا مفيد للغاية للمكتبات ذات بنية الإضافات (plugin)، مثل Express أو Fastify.
تخيل أنك تستخدم برنامجًا وسيطًا (middleware) في Express يضيف خاصية `user` إلى الكائن `Request`. بدون توسيع، ستشكو TypeScript من أن `user` غير موجود في `Request`.
إليك كيف يمكنك إخبار TypeScript بهذه الخاصية الجديدة:
// في ملف types/express.d.ts الخاص بك
// يجب علينا استيراد النوع الأصلي لتوسيعه
import { UserProfile } from './auth'; // بافتراض أن لديك نوع UserProfile
// نخبر TypeScript بأننا نوسع وحدة 'express-serve-static-core'
declare module 'express-serve-static-core' {
// نستهدف واجهة 'Request' داخل تلك الوحدة
interface Request {
// نضيف خاصيتنا المخصصة
user?: UserProfile;
}
}
الآن، في جميع أنحاء تطبيقك، سيتم كتابة كائن Express `Request` بشكل صحيح مع خاصية `user` الاختيارية، وستحصل على أمان نوعي كامل وإكمال تلقائي.
توجيهات الشرطة المائلة الثلاثية (Triple-Slash Directives)
قد ترى أحيانًا تعليقات في أعلى ملفات .d.ts تبدأ بثلاث شرطات مائلة (///). هذه هي توجيهات الشرطة المائلة الثلاثية، والتي تعمل كتعليمات للمترجم.
/// <reference types="..." />: هذا هو الأكثر شيوعًا. إنه يدرج صراحةً تعريفات أنواع حزمة أخرى كتبعية. على سبيل المثال، قد تتضمن أنواع إضافة WebdriverIO/// <reference types="webdriverio" />لأن أنواعها الخاصة تعتمد على أنواع WebdriverIO الأساسية./// <reference path="..." />: يستخدم هذا للإعلان عن تبعية لملف آخر داخل نفس المشروع. إنه نحو أقدم، وقد تم استبداله إلى حد كبير باستيراد وحدات ES.
أفضل الممارسات لإدارة ملفات التعريف
- تفضيل الأنواع المرفقة: عند الاختيار بين المكتبات، فضل تلك المكتوبة بـ TypeScript أو التي ترفق تعريفات الأنواع الرسمية الخاصة بها. إنه يشير إلى الالتزام بنظام TypeScript البيئي.
- احتفظ بـ
@typesفيdevDependencies: قم دائمًا بتثبيت حزم@typesباستخدام--save-devأو-D. ليست هناك حاجة إليها في كود الإنتاج الخاص بك. - مواءمة الإصدارات: من المصادر الشائعة للأخطاء عدم تطابق إصدار المكتبة مع إصدار
@typesالخاص بها. من المحتمل أن يكون لترقية إصدار رئيسي في مكتبة (على سبيل المثال، من v2 إلى v3) تغييرات كاسرة في واجهة برمجة التطبيقات الخاصة بها، والتي يجب أن تنعكس في حزمة@types. حاول إبقائها متزامنة. - استخدم
tsconfig.jsonللتحكم: يمكن أن تمنحك خيارات المترجمtypeRootsوtypesفيtsconfig.jsonتحكمًا دقيقًا في المكان الذي يبحث فيه TypeScript عن ملفات التعريف. يخبرtypeRootsالمترجم بالمجلدات التي يجب فحصها (افتراضيًا، هو./node_modules/@types)، ويسمح لكtypesبسرد حزم الأنواع التي يجب تضمينها بشكل صريح. - ساهم في المجتمع: إذا كتبت ملف تعريف شامل لمكتبة لا تملكه، ففكر في المساهمة به في مشروع DefinitelyTyped. هذه طريقة رائعة لرد الجميل لمجتمع المطورين العالمي ومساعدة الآلاف من الآخرين.
الخاتمة: الأبطال المجهولون للأمان النوعي
ملفات تعريف TypeScript هي الأبطال المجهولون الذين يجعلون من الممكن دمج عالم JavaScript الديناميكي المترامي الأطراف بسلاسة في بيئة تطوير قوية وآمنة من حيث النوع. إنها الحلقة الحاسمة التي تمكن أدواتنا، وتمنع عددًا لا يحصى من الأخطاء، وتجعل قواعد التعليمات البرمجية لدينا أكثر مرونة وتوثيقًا ذاتيًا.
من خلال فهم كيفية العثور على ملفات .d.ts واستخدامها وحتى إنشائها بنفسك، فأنت لا تقوم فقط بإصلاح خطأ في المترجم - بل ترتقي بسير عملك التطويري بأكمله. أنت تطلق العنان للإمكانات الكاملة لكل من TypeScript والنظام البيئي الغني لمكتبات JavaScript، مما يخلق تآزرًا قويًا ينتج عنه برامج أفضل وأكثر موثوقية لجمهور عالمي.